package me.yushuai.wukong.base.service.impl;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import lombok.Data;
import me.yushuai.wukong.base.api.dto.*;
import me.yushuai.wukong.base.convertor.ConfConvertor;
import me.yushuai.wukong.base.entity.Conf;
import me.yushuai.wukong.base.entity.ConfGroup;
import me.yushuai.wukong.base.repository.ConfGroupRepository;
import me.yushuai.wukong.base.repository.ConfRepository;
import me.yushuai.wukong.base.service.ConfService;
import me.yushuai.wukong.common.dto.PageDTO;
import me.yushuai.wukong.common.exceptin.BizIdNotExistException;
import me.yushuai.wukong.common.exceptin.DuplicateBizIdException;
import me.yushuai.wukong.common.exceptin.NotExistException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.convert.ConversionService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.persistence.criteria.Predicate;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;

/**
 * this is ConfService implement class. the implement using ConversionService of Spring to convertor value.
 * using guava cache to cache Conf by id or bizId.
 *
 * @author zhoushuai@189.cn
 * @date 2022-09-14
 */
@Service
public class ConfServiceImpl implements ConfService, InitializingBean {

    public static final int CACHE_MAXIMUM_SIZE = 10000;

    private Cache<ConfIdCacheKey, Conf> confIdCache;

    private Cache<ConfBizIdCacheKey, Conf> confBizIdCache;

    private Cache<ConfCacheKey, Boolean> compareValueCache;

    @Autowired
    private ConversionService conversionService;

    @Autowired
    private ConfConvertor confConvertor;

    @Autowired
    private ConfRepository confRepository;

    @Autowired
    private ConfGroupRepository confGroupRepository;

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = DuplicateBizIdException.class)
    public Long createConfGroup(CreateConfGroupDTO dto) throws DuplicateBizIdException {

        //1. checking the confCode if exists, if the confCode exist throw exception
        final String bizId = dto.getBizId();
        if (this.confGroupRepository.existsByBizId(bizId)) {
            throw new DuplicateBizIdException(bizId, ConfGroup.class);
        }

        //2. transition the confDTO to ConfBasic and save it.
        ConfGroup confGroup = this.confConvertor.mapToConfGroup(dto);
        this.confGroupRepository.save(confGroup);

        //4. return the confId
        return confGroup.getGroupId();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = NotExistException.class)
    public Long updateConfGroup(UpdateConfGroupDTO dto) throws NotExistException {
        //1. 检查系统配置编码是否存在
        final Long confId = dto.getGroupId();
        final ConfGroup confGroup = this.confGroupRepository.findById(confId)
                .orElseThrow(() -> new NotExistException(confId, ConfGroup.class));

        //2. 通过系统配置编号更新系统配置信息
        this.confConvertor.updateToConfGroup(dto, confGroup);
        this.confGroupRepository.save(confGroup);
        return dto.getGroupId();
    }

    @Override
    @Transactional(propagation = Propagation.NESTED, rollbackFor = NotExistException.class)
    public void deleteConfGroupById(Long confGroupId) throws NotExistException {
        //1. checking the confId if exists, if the confId not exist throw exception
        if (this.confGroupRepository.existsById(confGroupId)) {
            throw new NotExistException(confGroupId, ConfGroup.class);
        }

        //2. batch delete ConfItem by confId
        this.confRepository.deleteByGroupId(confGroupId);

        //3. delete ConfGroup by confId
        this.confGroupRepository.deleteById(confGroupId);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public ConfGroupDTO findConfGroupById(Long groupId) throws NotExistException {
        //1. 通过confId查询系统配置信息
        final ConfGroup confGroup = this.confGroupRepository.findById(groupId)
                .orElseThrow(() -> new NotExistException(groupId, ConfGroup.class));

        return this.confConvertor.mapToConfGroupDto(confGroup);
    }


    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public PageDTO<ConfGroupDTO> pagingSearchConfGroup(PagingSearchConfGroup pagingSearch) {
        //1. prepare query conditions
        Pageable pageable = PageRequest.of(pagingSearch.getCurrent(), pagingSearch.getSize());
        final Specification<ConfGroup> spec = (root, query, criteriaBuilder) -> {
            List<Predicate> list = new ArrayList<>();

            final String confType = pagingSearch.getConfType();
            if (StringUtils.hasText(confType)) {
                list.add(criteriaBuilder.equal(root.get("confType").as(String.class), confType));
            }

            final String[] confTypes = pagingSearch.getConfTypes();
            if (Objects.nonNull(confTypes) && confTypes.length > 0) {
                list.add(criteriaBuilder.in(root.get("confType").as(String.class)).in((Object[]) confTypes));
            }

            return criteriaBuilder.and(list.toArray(Predicate[]::new));
        };

        //2. execute query command
        final Page<ConfGroup> confGroupPage = this.confGroupRepository.findAll(spec, pageable);

        //3. transition ConfProfile to ConfProfileDTO and create PageDTO
        pagingSearch.setTotal(confGroupPage.getTotalElements());
        return PageDTO.of(pagingSearch, this.confConvertor.mapToConfGroupDtoList(confGroupPage.getContent()));
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)
    public Long createConf(CreateConfDTO dto) throws DuplicateBizIdException, NotExistException {
        //1. first checking the group id exists, if not exists than throw NotExistException
        final Long groupId = dto.getGroupId();
        if (!this.confGroupRepository.existsById(groupId)) {
            throw new NotExistException(groupId, "Conf.groupId");
        }

        //2. first checking the bizId if exists.
        final String bizId = dto.getBizId();
        this.assertNotExistConfBizId(groupId, bizId);

        //3. second transition from dto to Conf and save the conf
        final Conf conf = this.confConvertor.mapToConf(dto);
        this.confRepository.save(conf);
        return conf.getId();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)
    public Long updateConfDef(UpdateConfDefDTO dto) throws NotExistException {
        final Conf conf = getConfOrElseThrow(dto.getGroupId(), dto.getId());

        //3. 更新配置项信息
        this.confConvertor.updateConf(dto, conf);

        this.confRepository.save(conf);
        return dto.getId();
    }


    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Throwable.class)
    public Long updateConf(UpdateConfValueDTO dto) throws NotExistException {

        //1. first checking the confId and groupId exists, if not exists than throw exception
        final Long groupId = dto.getGroupId();
        final Long confId = dto.getId();
        final Conf conf = getConfOrElseThrow(groupId, confId);

        //2. second copy data form dto to Conf
        this.confConvertor.updateConf(dto, conf);

        //3. update conf and return confId
        this.confRepository.save(conf);
        return dto.getId();
    }


    @Override
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = {Throwable.class})
    public void deleteConf(Long groupId, Long confId) throws NotExistException {
        //1. 清除本地缓存
        this.confIdCache.invalidate(new ConfIdCacheKey(groupId, confId));

        //2. 检查是否删除成功
        if (!this.confRepository.existsByGroupIdAndId(groupId, confId)) {
            throw new NotExistException(confId, Conf.class);
        }

        //3. 删除配置项记录
        this.confRepository.deleteById(groupId, confId);
    }


    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public ConfDTO findConfById(Long groupId, Long confId) throws NotExistException {

        //1. 检查配置项信息是否存在并抛出异常
        final Conf conf = getConfOrElseThrow(groupId, confId);

        //2. 返回查询结果
        return this.confConvertor.mapToConfDto(conf);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public PageDTO<ConfDTO> pagingSearchConf(Long groupId, PagingSearchConf pagingSearchConf) {
        //1. first create query params
        final int curPage = pagingSearchConf.getCurrent() - 1;
        Pageable pageRequest = PageRequest.of(curPage, pagingSearchConf.getSize());
        final Specification<Conf> spec = (root, query, criteriaBuilder) -> {
            List<Predicate> list = new ArrayList<>();
            list.add(criteriaBuilder.equal(root.get("groupId").as(Long.class), groupId));

            final Predicate[] predicates = list.toArray(new Predicate[]{});
            return criteriaBuilder.and(predicates);
        };

        //2. second paging query
        final Page<Conf> confPage = this.confRepository.findAll(spec, pageRequest);

        //3. transition Conf to ConfDTO and create PageDTO and return it.
        pagingSearchConf.setTotal(confPage.getTotalElements());
        return PageDTO.of(pagingSearchConf, this.confConvertor.mapToConfDtoList(confPage.getContent()));
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public <T> T findValue(String groupBizId, String confBizId, Class<T> clazz) {
        return this.findValue(groupBizId, confBizId, null, clazz);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public <T> T findValue(String groupBizId, String confBizId, T defaultValue, Class<T> clazz) {
        String originalValue;
        try {
            Conf conf = getConfOrElseThrow(groupBizId, confBizId);
            originalValue = conf.getValue1();
        } catch (BizIdNotExistException e) {
            return defaultValue;
        }

        if (!this.conversionService.canConvert(String.class, clazz)) {
            return defaultValue;
        }

        final T sourceValue = this.conversionService.convert(originalValue, clazz);
        return Objects.nonNull(sourceValue) ? sourceValue : defaultValue;
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Boolean boolValue(String groupBizId, String confBizId, Boolean defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, Boolean.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Boolean boolValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, Boolean.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Integer intValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, Integer.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Integer intValue(String groupBizId, String confBizId, int defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, Integer.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Long longValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, Long.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Long longValue(String groupBizId, String confBizId, long defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, Long.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public String strValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, String.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public String strValue(String groupBizId, String confBizId, String defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, String.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Date dateValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, Date.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public Date dateValue(String groupBizId, String confBizId, Date defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, Date.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public LocalDate localDateValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, LocalDate.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public LocalDate localDateValue(String groupBizId, String confBizId, LocalDate defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, LocalDate.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public LocalDate localDateTimeValue(String groupBizId, String confBizId) {
        return this.findValue(groupBizId, confBizId, null, LocalDate.class);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public LocalDate localDateTimeValue(String groupBizId, String confBizId, LocalDate defaultValue) {
        return this.findValue(groupBizId, confBizId, defaultValue, LocalDate.class);
    }


    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean compareValue(String groupBizId, String confBizId, Object defaultValue, Object expectValue) {
        //1. first from local cache get compare value
        final ConfCacheKey cacheKey = new ConfCacheKey(groupBizId, confBizId, defaultValue, expectValue);
        if (Objects.nonNull(this.compareValueCache)) {
            final Boolean result = this.compareValueCache.getIfPresent(cacheKey);
            if (Objects.nonNull(result)) {
                return result;
            }
        }

        //2. second get conf
        final Class<?> targetClazz = expectValue.getClass();
        Object confValue = this.findValue(groupBizId, confBizId, targetClazz);
        confValue = Objects.isNull(confValue) ? defaultValue : confValue;

        //3. third equals confValue and expectValue
        return Objects.equals(confValue, expectValue);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean compareValue(String groupBizId, String confBizId, Object expectValue) {
        return this.compareValue(groupBizId, confBizId, null, expectValue);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean compareValue(String groupBizId, String confBizId, Object[] expectValues) {
        if (Objects.isNull(expectValues)) {
            return false;
        }

        for (Object expectValue : expectValues) {
            if (this.compareValue(groupBizId, confBizId, expectValue)) {
                return true;
            }
        }
        return false;
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public ConfDTO findConfByBizId(String groupBizId, String confBizId) throws BizIdNotExistException {
        final Conf conf = getConfOrElseThrow(groupBizId, confBizId);
        return this.confConvertor.mapToConfDto(conf);
    }


    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean checkExistsBizId(Long groupId, String confBizId) {
        return this.confRepository.existsByGroupIdAndBizId(groupId, confBizId);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean checkExistsConfId(Long groupId, Long confId) {
        //2. 返回查询结果
        return this.confRepository.existsByGroupIdAndId(groupId, confId);
    }

    @Override
    @Transactional(propagation = Propagation.NOT_SUPPORTED, readOnly = true)
    public boolean checkExistsGroupBizId(String groupBizId) {
        return this.confGroupRepository.existsByBizId(groupBizId);
    }

    @Override
    public List<ConfDTO> findByConfGroupId(Long confGroupId) {
        return null;
    }

    @Override
    public List<ConfDTO> findByConfGroupBizId(String confGroupBizId) {
        return null;
    }

    @Override
    public void afterPropertiesSet() {

        this.confIdCache = CacheBuilder.newBuilder()
                .maximumSize(CACHE_MAXIMUM_SIZE)
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .expireAfterAccess(30, TimeUnit.SECONDS)
                .build();

        this.confBizIdCache = CacheBuilder.newBuilder()
                .maximumSize(CACHE_MAXIMUM_SIZE)
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .expireAfterAccess(30, TimeUnit.SECONDS)
                .build();

        this.compareValueCache = CacheBuilder.newBuilder()
                .maximumSize(CACHE_MAXIMUM_SIZE)
                .expireAfterWrite(30, TimeUnit.SECONDS)
                .expireAfterAccess(30, TimeUnit.SECONDS)
                .build();

    }


    //---------------------------------------private method----------------------------------------


    private Conf getConfOrElseThrow(Long groupId, Long confId) throws NotExistException {

        //1. first from local cache get conf
        if (Objects.nonNull(this.confBizIdCache)) {
            Conf conf = this.confIdCache.getIfPresent(new ConfIdCacheKey(confId, groupId));
            if (Objects.nonNull(conf)) {
                return conf;
            }
        }

        //2. second from remove cache get conf

        //3. third from db get conf
        return this.confRepository.findById(groupId, confId)
                .orElseThrow(() -> new NotExistException(confId, Conf.class));
    }

    private Conf getConfOrElseThrow(String groupBizId, String confBizId) throws BizIdNotExistException {
        final ConfBizIdCacheKey bizIdCacheKey = new ConfBizIdCacheKey(groupBizId, confBizId);
        if (Objects.nonNull(this.confBizIdCache)) {
            final Conf cacheConf = this.confBizIdCache.getIfPresent(bizIdCacheKey);
            if (Objects.nonNull(cacheConf)) {
                return cacheConf;
            }
        }

        return this.confRepository.findByBizId(groupBizId, confBizId)
                .orElseThrow(() -> new BizIdNotExistException(confBizId, Conf.class));
    }

    /**
     * 断言ItemCode不存在
     *
     * @param groupId   配置编号
     * @param confBizId 配置项编码
     * @throws DuplicateBizIdException 当配置下的配置项编码已存在时抛出异常
     */
    private void assertNotExistConfBizId(Long groupId, String confBizId) throws DuplicateBizIdException {
        if (this.checkExistsBizId(groupId, confBizId)) {
            throw new DuplicateBizIdException(confBizId, Conf.class);
        }
    }

    @Data
    static class ConfCacheKey {

        private String groupBizId;
        private String confBizId;
        private Object defaultValue;
        private Object expectValue;

        public ConfCacheKey(String groupBizId, String confBizId, Object defaultValue, Object expectValue) {
            this.groupBizId = groupBizId;
            this.confBizId = confBizId;
            this.defaultValue = defaultValue;
            this.expectValue = expectValue;
        }
    }

    @Data
    static class ConfBizIdCacheKey {

        private String groupBizId;
        private String confBizId;

        public ConfBizIdCacheKey(String groupBizId, String confBizId) {
            this.groupBizId = groupBizId;
            this.confBizId = confBizId;
        }
    }

    @Data
    static class ConfIdCacheKey {

        private Long groupId;
        private Long confId;

        public ConfIdCacheKey(Long groupId, Long confId) {
            this.groupId = groupId;
            this.confId = confId;
        }
    }
}
